Skip to content

feat: give agents a Microsoft Entra identity across register, trace and evidence - #413

Merged
Paulo Lacerda (placerda) merged 2 commits into
developfrom
feat/agent365-identity
Aug 9, 2026
Merged

feat: give agents a Microsoft Entra identity across register, trace and evidence#413
Paulo Lacerda (placerda) merged 2 commits into
developfrom
feat/agent365-identity

Conversation

@placerda

Copy link
Copy Markdown
Contributor

Closes #353, #354, #355.

The gap

A trace could say what an agent did, but not which registered agent did it.
Nothing in the release evidence pack tied runtime behaviour back to an
accountable owner in the tenant, so "who owns this agent" was answerable only
out of band. The three issues approach the same loop from different ends, which
is why they are implemented together rather than as three separate PRs.

The loop

Register. agentops agent register --sponsor <upn> creates or adopts a
Microsoft Entra agent identity blueprint and writes the resolved id to
.agentops/identity/agent-identity.json. It is idempotent: an existing
blueprint with the same display name is adopted, not duplicated. A sponsor is
mandatory, because an identity with no named owner is not governable, which is
the entire point of registering one. --dry-run resolves and prints the inputs
without contacting Microsoft Graph.

Stamp. _resource_attributes() in the telemetry bootstrap resolves the id
and sets it as the OpenTelemetry resource attribute gen_ai.agent.id on every
span. When no identity is registered the attribute is omitted entirely rather
than set to an empty string, so its presence is itself a usable filter in a
trace query.

Publish. The release evidence pack gains an agent_identity section
reporting the id and whether it came from the local record or the environment. A
missing identity produces a warning, not a blocker, because adoption has to be
incremental.

A read-only Doctor check reports registration posture under Security. It
resolves the local record first, at zero cost, and contacts Microsoft Graph only
when identity.verify is set in agentops.yaml.

Four deviations from the literal issue text

Each was forced by the existing code, not chosen for convenience.

Issue said Implemented as Why
Add a rule under a governance pillar Standalone check called from analyzer.py Every RULE_REGISTRY rule takes (payload: AzureResourcesPayload, source_name), and run_posture_check returns early when resources.account is None. A registry rule would silently never run for a workspace with no Azure account resolved. Follows the existing governance.py standalone pattern.
Register during agentops ship agentops agent register, plus an opt-in step in the prod workflow templates There is no ship command. Ship is a pillar, documented in docs/ship.md and materialised as workflow templates.
Category governance Category.SECURITY No governance member exists in the Category enum, and adding one would reshape every consumer of the catalogue for a single check.
(not anticipated) New AgentIdentityConfig submodel in agentops.yaml AgentOpsConfig sets extra="forbid", so the identity key was rejected outright until declared. A permissive Dict[str, Any] would accept sponser: silently and fail later with a misleading "sponsor is required", so the submodel forbids extras too.

Why the workflow step is off by default

The generated production workflows carry the registration step gated on the
AGENTOPS_IDENTITY_SPONSOR repository variable. It writes to a tenant, so it
should never be the default behaviour of a workflow that AgentOps generates for
someone. Setting the variable to the sponsor's UPN turns it on.

Agent 365

The Agent 365 ingestion API is not public, so nothing here depends on it. The
handshake is documented instead, in docs/observe.md, so the id can be
correlated by whoever consumes the traces.

Tests

65 new unit tests across six files:

File Tests
test_agent_identity_service.py 28
test_agent_checks_agent_identity.py 13
test_cli_agent_register.py 8
test_evidence_pack_agent_identity.py 7
test_telemetry_agent_id.py 5
test_agentops_config_identity.py 4

Full suite: 1271 passed, 6 skipped. Both generated prod workflow templates
(placeholder and azd modes) were regenerated and verified to parse as valid
YAML with the identity steps correctly gated.

Docs

  • docs/observe.md — new "Agent identity on traces" section with the
    three-step handshake, the identity config block, and a KQL query.
  • docs/doctor-checks.mdgraph added to the data sources table, three
    agent_identity.* rows in the Security catalogue, plus a note that the checks
    are read-only.
  • docs/ship.md — "Giving the agent its own identity" under Identity and
    access, explaining the opt-in workflow variable.

…nd evidence

Closes #353, #354, #355.

A trace could say what an agent did but not which registered agent did it.
Nothing in the release evidence pack tied runtime behaviour back to an
accountable owner in the tenant, so "who owns this agent" was answerable only
out of band. These three issues asked for the same loop from different ends,
and they only make sense implemented together.

The loop has three points.

Register. `agentops agent register --sponsor <upn>` creates or adopts a
Microsoft Entra agent identity blueprint and writes the resolved id to
`.agentops/identity/agent-identity.json`. It is idempotent: an existing
blueprint with the same display name is adopted rather than duplicated. A
sponsor is mandatory, because an identity with no named owner is not
governable, which is the whole point of registering it. `--dry-run` resolves
and prints the inputs without contacting Microsoft Graph.

Stamp. `_resource_attributes()` in the telemetry bootstrap resolves the id and
sets it as the OpenTelemetry resource attribute `gen_ai.agent.id` on every span.
When no identity is registered the attribute is omitted entirely rather than set
to an empty string, so its presence is itself a usable filter in a trace query.

Publish. The release evidence pack gains an `agent_identity` section reporting
the id and whether it came from the local record or the environment. A missing
identity produces a warning, not a blocker, because adoption has to be
incremental.

A read-only Doctor check reports registration posture under the Security
category. It resolves the local record first, at zero cost, and only contacts
Microsoft Graph when `identity.verify` is set in `agentops.yaml`.

Four design decisions deviate from the literal issue text, each forced by the
existing code rather than chosen:

- The posture check is standalone rather than a `RULE_REGISTRY` rule. Every
  rule in that registry takes `(payload: AzureResourcesPayload, source_name)`,
  and `run_posture_check` returns early when `resources.account is None`, so a
  registry rule would silently never run for a workspace with no Azure account
  resolved. It follows the `governance.py` standalone pattern and is called
  from `analyzer.py`.
- Registration is `agentops agent register`, not a step inside `agentops ship`.
  There is no `ship` command. Ship is a pillar, documented in `docs/ship.md` and
  materialised as workflow templates. The templates now carry the step, gated on
  the `AGENTOPS_IDENTITY_SPONSOR` repository variable so it stays opt-in: it
  writes to a tenant, which should never be the default behaviour of a generated
  workflow.
- Findings use `Category.SECURITY`. There is no `governance` member in the
  `Category` enum, and adding one would reshape every consumer of the catalogue
  for a single check.
- `agentops.yaml` needed a typed `AgentIdentityConfig` submodel rather than a
  free-form dict. `AgentOpsConfig` sets `extra="forbid"`, so the `identity` key
  was rejected outright until it was declared. A permissive `Dict[str, Any]`
  would have accepted `sponser:` silently and failed much later with a
  misleading "sponsor is required", so the submodel also forbids extras.

The Agent 365 ingestion API is not public, so nothing here depends on it. The
handshake is documented instead, in `docs/observe.md`, so the id can be
correlated by whoever consumes the traces.

65 unit tests added across six files. Full suite: 1271 passed, 6 skipped.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bcb9c0b6-d506-46dc-90d2-8120413166ee
The resource attribute assembly moved into _resource_attributes(), which
imports agentops itself. The import at the init_tracing call site became dead
and ruff F401 flagged it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bcb9c0b6-d506-46dc-90d2-8120413166ee
@placerda
Paulo Lacerda (placerda) merged commit fb4f853 into develop Aug 9, 2026
13 checks passed
@placerda
Paulo Lacerda (placerda) deleted the feat/agent365-identity branch August 9, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant